/**
 * 
 */
package ch.comtools.servermanager.ssh.ui.terminal;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.widgets.Display;

import ch.comtools.servermanager.common.ansi.ANSI;
import ch.comtools.servermanager.common.resource.ResourceManager;

/**
 * @author Roger Dudler <roger.dudler@gmail.com>
 * @version $Id: RemoteConsumer.java 61 2007-04-19 18:31:36Z roger.dudler $
 */
/**
 * This thread consumes output from the remote server and displays it in
 * the terminal window.
 *
 */
class RemoteConsumer extends Thread {
	
	public static final int MODE_OVERWRITE = 1;
	public static final int MODE_SMART_INSERT = 2;
	
	private int pos = 0;
	private int screenWidth = 0;
	private int screenHeight = 0;
	private int mode = MODE_SMART_INSERT;
		
	private StyledText text;
	private InputStream input;
	private StringBuffer buffer = new StringBuffer();
	private List<Character> command = new ArrayList<Character>();
	private List<StyleRange> styleRanges = new ArrayList<StyleRange>();
	
	/**
	 * Create a new {@link RemoteConsumer} thread.
	 * @param input
	 * @param text
	 */
	public RemoteConsumer(InputStream input, StyledText text) {
		this.text = text;
		this.input = input;
	}
	
	/**
	 * Returns <code>true</code>, if control character is directly renderable 
	 * by styled text widget.
	 * @param c
	 * @return
	 */
	private boolean isRenderable(char c) {
		switch (c) {
			case '\r':
			case '\n':
			case '\t':
				return true;
		}
		return false;
	}
	
	/**
	 * @param data
	 * @param len
	 */
	private void addText(byte[] data, int len) {
		buffer = new StringBuffer();
		for (int i = 0; i < len; i++) {
			final char c = (char) (data[i] & 0xff);
			
			// esc
			if (c == 27) {
				// start of an escape sequence
				command.add(c);
				continue;
			}
			
			// command execute
			if (!command.isEmpty()) {
				command.add(c);
				if (this.executeCommand(command)) {
					command.clear();
				}
				continue;
			}
			
			// skip invisible control sequences
			if (c < 32) {
				if (!this.isRenderable(c)) {
					// System.out.println("control sequence @ " + i);
					continue;
				}
			}
			buffer.append(c);
		}
		
		// update ui
		Display.getDefault().syncExec(new Runnable() {
			public void run() {
				System.out.println("run");
				if (mode == MODE_OVERWRITE) {
					text.insert(buffer.toString());
				} else {
					text.append(buffer.toString());
				}
				text.setSelection(text.getText().length());
				if (styleRanges.size() > 0) {
					StyleRange[] x = (StyleRange[]) styleRanges.toArray(new StyleRange[0]);
					text.setStyleRanges(x);
				}
				pos = text.getText().length();
				/*
				if (text.getCharCount() >= 20000) {
					int toRemove = text.getCharCount() - 5000;
					text.replaceTextRange(0, toRemove, "");
				}
				*/
			}
		});
		
	}
	
	/**
	 * Execute a command.
	 * <b>Currently supported:</b>
	 * <ul>
	 * <li><code>ESC[#;#H</code> - Moves cusor to line #, column #</li>
	 * <li><code>ESC[#;#;....;#m</code> - Set graphics rendering mode</li>
	 * <li><code>ESC[J</code> - Clear screen and home cursor</li>
	 * </ul>
	 * <b>Not supported:</b>
	 * <ul>
	 * <li>none</li>
	 * </ul>
	 * @param command
	 * @return
	 */
	private boolean executeCommand(List<Character> command) {
		boolean isCommand = false;
		StringBuilder builder = new StringBuilder();
		for (char chr : command) {
			if (chr != 27) {
				builder.append(chr);
			}
		}
		String cmd = builder.toString();
		// System.out.println("maybe command: " + cmd);
		
		// ESC[#;#H - Moves cusor to line #, column #
		// Sets the cursor position where subsequent text will begin
		if (cmd.startsWith("[") && cmd.endsWith("H")) {
			if (cmd.length() > 2) {
				String[] parts = cmd.substring(1, cmd.length()-1).split(";");
				if (parts.length == 2) {
					try {
						this.setCursorPosition(Integer.valueOf(parts[0]), Integer.valueOf(parts[1]));
					} catch (NumberFormatException e) {
						// NOOP
					}
				} else {
					this.setCursorPosition(0, 0);
				}
			}
			isCommand = true;
		}
		
		// ESC[=#;7h or
		// ESC[=h or
		// ESC[=0h or  
		// ESC[?7h
		// Put screen in indicated mode where # is
		if (cmd.startsWith("[") && cmd.endsWith("h")) {
			try {
				int mode = Integer.valueOf(cmd.substring(2,3));
				this.setScreenMode(mode);
			} catch (NumberFormatException e) {
				// NOOP
			}
		}
		
		// ESC[J / ESC [2J - Clear screen and home cursor
		if (cmd.startsWith("[") && cmd.endsWith("J")) {
			System.out.println(cmd);
			this.doClearScreen();
			isCommand = true;
		}
		if (cmd.equals("[K")) {
			this.doBackspace();
			isCommand = true;
		}
		// set graphics rendition ESC[#;#;....;#m
		if (cmd.startsWith("[") && cmd.endsWith("m")) {
			if (cmd.length() > 2) {
				String[] parts = cmd.substring(1, cmd.length()-1).split(";");
				this.setGraphicRenderingModes(parts);
			}
			isCommand = true;
		}
		if (isCommand) {
			// System.out.println("Command: " + builder.toString() + " @ position: " + pos);
		}
		return isCommand;
	}
	
	/**
	 * Sets the cursor position where subsequent text will begin.
	 * Typically invoked by <code><ESC>[{ROW};{COLUMN}H</code>
	 * @param x x position
	 * @param y y position
	 */
	private void setCursorPosition(final int row, final int col) {
		System.out.println("set cursor position to: " + row + " (row) :" + col + " (col)");
		Display.getDefault().syncExec(new Runnable() {
			public void run() {
				text.setSelection(text.getText().length() - screenWidth * screenHeight);
				/*
				text.setSelection(text.getText().length());
				text.getContent().replaceTextRange(text.getSelectionRange().x - 1, 1, "");
				*/
			}
		});
	}
	
	/**
	 * Set screen mode.
	 * <ul>
	 *  <li>0 - 40 x 25 (monochrome)</li>
	 *  <li>1 - 40 x 25 (color)</li>
	 *  <li>2 - 80 x 25 (monochrome)</li>
	 *  <li>3 - 80 x 25 (color)</li>
	 *  <li>4 - 320 x 200 (color) graphics</li>
	 *  <li>5 - 320 x 200 (monochrome) graphics</li>
	 *  <li>6 - 640 x 200 (monochrome) graphics</li>
	 *  <li>7 - wrap at end of line</li>
	 * </ul>
	 * @param mode
	 */
	private void setScreenMode(int mode) {
		System.out.println("Entering screen mode: " + mode);
		switch (mode) {
			case 1:
				System.out.println("Mode: 40 x 25 (color)");
				screenWidth = 40;
				screenHeight = 25;
				this.addEmptyArea(40, 25);
				this.mode = MODE_OVERWRITE;
				break;
		}
	}
	
	/**
	 * Fill Screen content with empty strings.
	 * @param col
	 * @param row
	 */
	private void addEmptyArea(final int col, final int row) {
		Display.getDefault().syncExec(new Runnable() {
			public void run() {
				for (int i = 0; i < row; i++) {
					text.append(String.valueOf(createString(col, (char) 0xA0)));
					text.append("\n");
				}
			}
		});
	}
	
	
	/**
	 * Set rendering modes. <code>null</code> modes will be ignored.
	 * <ul>
     *  <li>00 - {@link ANSI#DISPLAY_ATTRIBUTE_RESET}</li>
     *  <li>01 - {@link ANSI#DISPLAY_ATTRIBUTE_FONT_STYLE_BOLD}</li>
     *  <li>30 - {@link ANSI#DISPLAY_ATTRIBUTE_FOREGROUND_COLOR_BLACK}</li>
     *  <li>31 - {@link ANSI#DISPLAY_ATTRIBUTE_FOREGROUND_COLOR_RED}</li>
     *  <li>32 - {@link ANSI#DISPLAY_ATTRIBUTE_FOREGROUND_COLOR_GREEN}</li>
     *  <li>33 - {@link ANSI#DISPLAY_ATTRIBUTE_FOREGROUND_COLOR_YELLOW}</li>
     *  <li>34 - {@link ANSI#DISPLAY_ATTRIBUTE_FOREGROUND_COLOR_BLUE}</li>
     *  <li>35 - {@link ANSI#DISPLAY_ATTRIBUTE_FOREGROUND_COLOR_MAGENTA}</li>
     *  <li>36 - {@link ANSI#DISPLAY_ATTRIBUTE_FOREGROUND_COLOR_CYAN}</li>
     *  <li>37 - {@link ANSI#DISPLAY_ATTRIBUTE_FOREGROUND_COLOR_WHITE}</li>
     *  <li>42 - {@link ANSI#DISPLAY_ATTRIBUTE_BACKGROUND_COLOR_GREEN}</li>
     * </ul>
     * <b>Not implemented:</b>
     * <ul>
     *  <li>02 - faint (or just 2)</li>
     *  <li>03 - standout (or just 3)</li>
     *  <li>04 - underline (or just 4)</li>
     *  <li>05 - blink on (or just 5)</li>
     *  <li>07 - reverse video on (or just 7)</li>
     *  <li>08 - nondisplayed (invisible) (or just 8)</li>
     *  <li>22 - normal</li>
     *  <li>23 - no-standout</li>
     *  <li>24 - no-underline</li>
     *  <li>25 - no-blink</li>
     *  <li>27 - no-reverse</li>
     *  <li>39 - default foreground</li>
     *  <li>40 - black background</li>
     *  <li>41 - red background</li>
     *  <li>43 - yellow background</li>
     *  <li>44 - blue background</li>
     *  <li>45 - magenta background</li>
     *  <li>46 - cyan background</li>
     *  <li>47 - white background</li>
     *  <li>49 - default background</li>
     * </ul>
     * 
	 * @param modes
	 */
	private void setGraphicRenderingModes(String[] modes) {
		Color foreground = null;
		Color background = null;
		int style = SWT.NORMAL;
		for (String mode : modes) {
			if (mode == null) {
				continue;
			}
			if (mode.equals("01") || mode.equals("1")) { // bold
				// TODO: customizable: style = SWT.BOLD;
			}
			if (mode.equals("30")) { // black foreground
				foreground = ResourceManager.getInstance().getColor(0, 0, 0);
			}
			if (mode.equals("31")) { // red foreground
				foreground = ResourceManager.getInstance().getColor(187, 0, 0);
			}
			if (mode.equals("32")) { // green foreground
				foreground = ResourceManager.getInstance().getColor(0, 187, 0);
			}
			if (mode.equals("33")) { // yellow foreground
				foreground = ResourceManager.getInstance().getColor(187, 187, 0);
			}
			if (mode.equals("34")) { // blue foreground
				foreground = ResourceManager.getInstance().getColor(0, 0, 187);
			}
			if (mode.equals("35")) { // magenta foreground
				background = ResourceManager.getInstance().getColor(187, 0, 187);
			}
			if (mode.equals("36")) { // cyan foreground
				foreground = ResourceManager.getInstance().getColor(0, 187, 187);
			}
			if (mode.equals("37")) { // white foreground
				foreground = ResourceManager.getInstance().getColor(255, 255, 255);
			}
			if (mode.equals("42")) { // green foreground
				background = ResourceManager.getInstance().getColor(0, 187, 0);
			}
		}
		this.addStyle(foreground, background, style);
	}
	
	/**
	 * @param length
	 * @param filler
	 * @return
	 */
	private char[] createString(int length, char filler) {
		char[] characters = new char[length];
		for (int i = 0; i < length; i++) {
			characters[i] = filler;
		}
		return characters;
	}
	
	/**
	 * 
	 */
	private void closeStyle() {
		int realpos = this.pos + this.buffer.length();
		if (styleRanges.size() > 0) {
			styleRanges.get(styleRanges.size()-1).length = realpos - styleRanges.get(styleRanges.size()-1).start;
		}
	}
	
	/**
	 * @param foreground
	 * @param background
	 */
	private void addStyle(Color foreground, Color background, int style) {
		this.closeStyle();
		int realpos = this.pos + this.buffer.length();
		StyleRange range = new StyleRange(realpos, 0, foreground, background, style);
		this.styleRanges.add(range);
	}
	
	/**
	 * Do backspace.
	 */
	private void doBackspace() {
		Display.getDefault().syncExec(new Runnable() {
			public void run() {
				text.setSelection(text.getText().length());
				text.getContent().replaceTextRange(text.getSelectionRange().x - 1, 1, "");
			}
		});
	}
	
	/**
	 * Do clear screen.
	 */
	@SuppressWarnings("unused")
	private void doClearScreen() {
		Display.getDefault().syncExec(new Runnable() {
			public void run() {
				buffer = new StringBuffer();
				styleRanges.clear();
				pos = 0;
				text.setText("");
			}
		});
	}
	
	/* (non-Javadoc)
	 * @see java.lang.Thread#run()
	 */
	public void run() {
		int bufferSize = 8192;
		byte[] buf = new byte[bufferSize];
		try {
			while (true) {
				int len = input.read(buf);
				if (len == -1)
					return;
				this.addText(buf, len);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}
